package net.zxteam.solutions.java.rs_sync;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.PrintStream;
import java.util.HashMap;

import javax.xml.bind.JAXBException;

import net.zxteam.reusable.java.IllegalArgumentNullException;

import org.tigris.subversion.javahl.ChangePath;
import org.tigris.subversion.javahl.ClientException;
import org.tigris.subversion.javahl.DirEntry;
import org.tigris.subversion.javahl.Info;
import org.tigris.subversion.javahl.LogMessage;
import org.tigris.subversion.javahl.NodeKind;
import org.tigris.subversion.javahl.PropertyData;
import org.tigris.subversion.javahl.SVNClient;

import org.tigris.subversion.javahl.Revision;
import org.tmatesoft.svn.core.SVNDepth;

public class SVNRSSyncImplAllRepository extends SVNRSSyncBase {

	private final PrintStream log = System.out;
	
	private final String propNameInitInfo = "rs-sync:init";
	private final String propNameSyncInfo = "rs-sync:sync";
	private final String propNameRevInfo = "rs-sync:info";
	private final String propNameStub = "rs-sync:stub";

	private InitInfo _initInfo = null;
	private SyncInfo _syncInfo = null;
	private long _mergingRevision = -1;
	
	protected SVNRSSyncImplAllRepository() throws IOException {

		super();
	}
	
	@Override
	public void init(String sourceRepositoryUrl, long sourceRevision) throws RSSyncException {
		
		try {
			String destinationUrl = getDestinationRepositoryUrl();

			// Verify! Destination RepositoryUrl folder should be empty or should be not exist!
			// Probe to read info
			Info destInfo = null;
			try { destInfo = svnDestination.info(destinationUrl); }
			catch(ClientException ex) {
				//int errCode = ex.getAprError();
				//if(errCode == 150000 || errCode == 17000) 
			}

			HashMap<String, String> revpropTable = new HashMap<String, String>();

			Info svnInfo = svnSource.info(sourceRepositoryUrl);

			boolean isAllRight = false;
			if(destInfo != null) {	// path not found in repository! It is OK!

				DirEntry[] list = svnDestination.list(destinationUrl, Revision.HEAD, false);
				if(list == null || list.length == 0) {	// No files, so we test rs-sync info. May be "init" already executed

					if(svnDestination.propertyGet(destinationUrl, this.propNameInitInfo, Revision.HEAD) == null) {

						Info lastRevInfo = svnDestination.info(destinationUrl);
						if(svnDestination.revProperty(destinationUrl, this.propNameSyncInfo, Revision.getInstance(lastRevInfo.getLastChangedRevision())) == null) {

							isAllRight = true;
						}
					}
				}
			}
			else {

				svnDestination.mkdir(new String[] { destinationUrl }, "rs-sync: create folder", true, null);
				isAllRight = true;
			}

			if(isAllRight) {

				long sourceRevisionToMerge = FindNearLargeRevision(svnSource, sourceRepositoryUrl, sourceRevision, sourceRevision <= 0);
				
				this.prepareLocalWorkCopy(svnSource, sourceRepositoryUrl, Revision.getInstance(sourceRevisionToMerge), this.sourceLocalCopyPath);
				this.prepareLocalWorkCopy(svnDestination, destinationUrl, Revision.HEAD, this.destinationLocalCopyPath);

				String initMessage = getWorkCopyLogMessage(svnSource, this.sourceLocalCopyPath).getMessage() + " (rs-sync [init])";
				
				this.cloneRepositoryFiles(sourceLocalCopyPath, destinationLocalCopyPath);

				_syncInfo = new SyncInfo();
				_syncInfo.revision = sourceRevisionToMerge;
				String syncStr = SyncInfo.serialize(_syncInfo);

				_initInfo = new InitInfo();
				_initInfo.revision = sourceRevision;
				_initInfo.repositoryRoot = svnInfo.getRepository();
				_initInfo.relativePath = sourceRepositoryUrl.replaceFirst(svnInfo.getRepository(), "");
				String initStr = InitInfo.serialize(_initInfo);

				revpropTable.put(this.propNameSyncInfo, syncStr);

				svnDestination.propertySet(this.destinationLocalCopyPath.getAbsolutePath(), this.propNameInitInfo, initStr, false);

				svnDestination.commit(new String[] { this.destinationLocalCopyPath.getAbsolutePath() }, initMessage, SVNDepth.INFINITY.getId(), false, false, null, revpropTable);
			}
			else {

				throw new IllegalStateException("Invalid destination repository");
			}
		}
		catch(Exception ex) { throw new RSSyncException(ex); }
	}

	@Override
	public void sync() throws RSSyncException {

		try{

			PropertyData initRevPropData = svnDestination.propertyGet(this.getDestinationRepositoryUrl(), this.propNameInitInfo, Revision.HEAD);
			if(initRevPropData == null)
				throw new IllegalStateException("Invalid destination repository");

			do {

				Info lastRevInfo = svnDestination.info(this.getDestinationRepositoryUrl());
				PropertyData lastRevSyncData = svnDestination.revProperty(this.getDestinationRepositoryUrl(), this.propNameSyncInfo, Revision.getInstance(lastRevInfo.getLastChangedRevision()));
				if(lastRevSyncData == null)
					throw new IllegalStateException("Invalid destination repository");
				_mergingRevision = -1;

				_initInfo = InitInfo.deserialize(initRevPropData.getValue());
				_syncInfo = SyncInfo.deserialize(lastRevSyncData.getValue());

				String sourceRepositoryUrl = _initInfo.getRepositoryUrl();
				_mergingRevision = FindNearLargeRevision(svnSource, sourceRepositoryUrl, _syncInfo.revision, false);

				if(_mergingRevision >= 0) {
					
					log.println("Sync revision " + _mergingRevision + " from " + sourceRepositoryUrl + " is statring...");
					this.sync(sourceRepositoryUrl, _mergingRevision);
					log.println("Sync revision " + _mergingRevision + " from " + sourceRepositoryUrl + " is complete");

					log.print("Timeout. Start at");
					for(int i = 5; i > 0; --i) { log.print(" " + i); Thread.sleep(1000); }
					log.println("...\tOk");
				}
			} while(_mergingRevision >= 0);
		}
		catch (Exception e) { throw new RSSyncException(e); }
	}

	private void cloneRepositoryFiles(File sourceWorkCopy, File destWorkCopy) throws IOException, ClientException, JAXBException {

		// TODO instantiate copy from remote repository. Because may be svn:external
		copyWorkCopyDirectoryContent(sourceWorkCopy, destWorkCopy);
	}

	private void copyWorkCopyDirectoryContent(File sourceDir, File destDir) throws IOException, ClientException, JAXBException {

		if(sourceDir == null || !sourceDir.isDirectory() || !net.zxteam.reusable.java.io.Directory.exists(sourceDir)) throw new IllegalArgumentNullException("sourceDir");
		if(destDir == null) throw new IllegalArgumentNullException("destDir");

		if(!net.zxteam.reusable.java.io.Directory.exists(destDir)) {
			 net.zxteam.reusable.java.io.Directory.createDirectory(destDir);

			 svnDestination.add(destDir.getAbsolutePath(), false);
		}

		File[] children = sourceDir.listFiles();
		for(File sourceChild : children) {

			if(".svn".equals(sourceChild.getName()))
				continue;

			File destChild = new File(destDir, sourceChild.getName());
			if(sourceChild.isFile()) {	// coping file

				FileInputStream fis = new FileInputStream(sourceChild);
				FileOutputStream fos = new FileOutputStream(destChild);
				byte[] buf = new byte[1024];
				int i = 0;
				while((i=fis.read(buf))!=-1) { fos.write(buf, 0, i); }
				fis.close();
				fos.close();

				svnDestination.add(destChild.getAbsolutePath(), false);
			}
			else if(sourceChild.isDirectory()) {

				copyWorkCopyDirectoryContent(sourceChild, destChild);
			}
			else
				throw new IllegalStateException();

			this.mergeSvnProperties(sourceChild, destChild);

			// Set revision info property
			Info svnInfo = svnSource.info(sourceChild.getAbsolutePath());
			RevisionInfo revInfo = new RevisionInfo();
			revInfo.source = svnInfo.getUrl();
			revInfo.revision = svnInfo.getRevision();
			svnDestination.propertySet(destChild.getAbsolutePath(), this.propNameRevInfo, RevisionInfo.serialize(revInfo), false);
		}
	}
	
	private void mergeSvnProperties(File sourceWorkCopy, File destWorkCopy) throws ClientException {

		// Remove all current properties
		PropertyData[] propertiesToRemove = svnDestination.properties(destWorkCopy.getAbsolutePath());
		if(propertiesToRemove != null && propertiesToRemove.length > 0) {

			for(PropertyData p : propertiesToRemove) {

				if(!p.getName().startsWith("rs-sync"))	// skip rs-sync property
					svnDestination.propertyRemove(destWorkCopy.getAbsolutePath(), p.getName(), false);
			}
		}

		// Copy properties from source
		PropertyData[] properties = svnSource.properties(sourceWorkCopy.getAbsolutePath());
		if(properties != null && properties.length > 0) {

			for(PropertyData p : properties) {

				svnDestination.propertySet(destWorkCopy.getAbsolutePath(), p.getName(), p.getData(), false);
			}
		}
	}
	
	private long FindNearLargeRevision(SVNClient svn, String url, long curRevision, boolean includeCurRevision) throws ClientException {

		LogMessage[] messages = svn.logMessages(url, Revision.getInstance(curRevision), Revision.HEAD, false, false, includeCurRevision ? 1 : 2);
		if(messages != null) {

			if(messages.length > 0) {

				if(includeCurRevision) {

					return messages[0].getRevisionNumber();
				}
				
				if(messages.length == 2)
					return messages[1].getRevisionNumber();
			}
		}
		
		return -1;
	}

	private void sync(String sourceUrl, long sourceRevToMerge) throws Exception {

		LogMessage[] messages = svnSource.logMessages(sourceUrl, Revision.getInstance(_mergingRevision), Revision.HEAD, false, true, 1);
		if(messages != null && messages.length > 0) {



			log.println("Prepare destination local copy...");
			this.prepareLocalWorkCopy(svnDestination, this.getDestinationRepositoryUrl(), Revision.HEAD, destinationLocalCopyPath);

			log.println("Prepare source local copy...");
			this.prepareLocalWorkCopy(svnSource, sourceUrl, Revision.getInstance(sourceRevToMerge), sourceLocalCopyPath);

			log.println("Reading changes...");
			LogMessage msg = svnSource.logMessages(sourceLocalCopyPath.getAbsolutePath(), 
					Revision.WORKING, null, false, true, 1)[0];

			ChangePath[] changes = msg.getChangedPaths();
			log.println("The " + changes.length + " change(s) was(re) found...");
			for(ChangePath changeItem : changes) {

				if(changeItem.getPath().startsWith(_initInfo.relativePath))
				{
					log.println("+ .. merge item: " + changeItem.getPath());
					mergeOneChanges(changeItem);
				}
				else
					log.println("! .. skip item: " + changeItem.getPath());
			}

			String finalMessage = msg.getMessage();// + "\r\n -- \r\nRevision: " + sourceRevToMerge + "\r\nAuthor: " + msg.getAuthor() + "\r\nDate: " + msg.getDate();

			SyncInfo si = new SyncInfo();
			si.revision = sourceRevToMerge;
			String siStr = SyncInfo.serialize(si);

			HashMap<String, String> revpropTable = new HashMap<String, String>();
			revpropTable.put(this.propNameSyncInfo, siStr);

			long cRes = -1;
			if(changes.length == 0) { // if revision without changes, creating stub

				log.println("Set stub revProperty to " + destinationLocalCopyPath.getAbsolutePath());
				PropertyData pd = svnDestination.propertyGet(destinationLocalCopyPath.getAbsolutePath(), this.propNameStub);
				long stubCount = 1;
				if(pd != null) stubCount = Long.parseLong(pd.getValue()) + 1;
				svnDestination.propertySet(destinationLocalCopyPath.getAbsolutePath(), this.propNameStub, "" + stubCount, false);
			}

			log.println("Commit changes to " + destinationLocalCopyPath.getAbsolutePath());
			cRes = svnDestination.commit(new String[] { destinationLocalCopyPath.getAbsolutePath() }, finalMessage, SVNDepth.INFINITY.getId(), false, false, null, revpropTable);
			if(cRes == -1)
				throw new Exception("SVN Commit returned -1");

		}
		else
			throw new Exception("Hmm...");
	}

	private LogMessage getWorkCopyLogMessage(SVNClient svn, File workCopy) throws ClientException {

		LogMessage[] messages = svn.logMessages(workCopy.getAbsolutePath(), Revision.WORKING, Revision.WORKING);
		if(messages == null || messages.length != 1)
			throw new IllegalStateException("Could not get work copy revision");

		return messages[0];
	}

	private long FindLocalDestinationRelativeRevisionForBranching(long sourceRevision) throws ClientException, JAXBException {

		LogMessage[] messages = svnDestination.logMessages(destinationLocalCopyPath.getAbsolutePath(), Revision.START, Revision.HEAD);
		long prevRevision = -1;
		for(LogMessage msg : messages) {

			PropertyData pd = svnDestination.revProperty(destinationLocalCopyPath.getAbsolutePath(), this.propNameSyncInfo, Revision.getInstance(msg.getRevisionNumber()));
			if(pd != null) {

				SyncInfo si = SyncInfo.deserialize(pd.getValue());
				
				long curRevision = si.revision;
				if(curRevision == sourceRevision)
					return msg.getRevisionNumber();
				else if(curRevision > sourceRevision)
					if(prevRevision > 0)
						return prevRevision;
					else
						throw new IllegalStateException("FindLocalDestinationRelativeRevisionForBranching : Relative revision not found. May be repository mirror is corrupt");
	
				prevRevision = msg.getRevisionNumber();
			}
		}

		throw new IllegalStateException("FindLocalDestinationRelativeRevisionForBranching : Relative revision not found");
	}
	
	private void mergeOneChanges(ChangePath changeItem) throws ClientException, IOException, JAXBException {

		if(changeItem == null)
			throw new IllegalArgumentException();

		char action = changeItem.getAction();
		switch (action) {
		case 'A':	// Add action
			mergeAction_Add(changeItem);
			break;

		case 'M':	// Modify action
			mergeAction_Modify(changeItem);
			break;

		case 'D':	// Delete action
			mergeAction_Delete(changeItem);
			break;

		default:
			throw new IllegalStateException("mergeOneChanges: Action '" + action + "' is not supported.");
		}
	}
	
	private void mergeAction_Add(ChangePath changeItem) throws ClientException, IOException, JAXBException {

		String itemPath = changeItem.getPath().replace(_initInfo.relativePath, "");
		if("".equals(itemPath))
			throw new IllegalStateException("mergeAction_Add : no file to add");

		File sourceLocalPath = new File(sourceLocalCopyPath, itemPath);
		File destLocalPath = new File(destinationLocalCopyPath, itemPath);

		if(changeItem.getCopySrcPath() != null && changeItem.getCopySrcPath().startsWith(_initInfo.relativePath)) { // branching

			String itemBranchSrcPath = changeItem.getCopySrcPath().replace(_initInfo.relativePath, "");
			if("".equals(itemBranchSrcPath))
				throw new IllegalStateException("mergeAction_Add : no file to branch");

			File branchSourceLocalPath = new File(destinationLocalCopyPath, itemBranchSrcPath);
			long destBranchRev = FindLocalDestinationRelativeRevisionForBranching(changeItem.getCopySrcRevision());
			svnDestination.copy(branchSourceLocalPath.getAbsolutePath(), destLocalPath.getAbsolutePath(), null
				, Revision.getInstance(destBranchRev));
		}
		else {	// creating

			int kind = changeItem.getNodeKind();
			if(kind == NodeKind.unknown) {	// old version of svn server not set kind, try get it from svnInfo

				Info svnInfo = svnSource.info(sourceLocalPath.getAbsolutePath());
				kind = svnInfo.getNodeKind();
			}
			
			switch(kind) {
			case NodeKind.dir:

				svnDestination.mkdir(new String[] { destLocalPath.getAbsolutePath() }, null, false, null);
				this.mergeSvnProperties(sourceLocalPath, destLocalPath);

				break;

			case NodeKind.file:

				FileInputStream fis = new FileInputStream(sourceLocalPath);
				FileOutputStream fos = new FileOutputStream(destLocalPath);
				byte[] buf = new byte[1024];
				int i = 0;
				while((i=fis.read(buf))!=-1) { fos.write(buf, 0, i); }
				fis.close();
				fos.close();

				svnDestination.add(destLocalPath.getAbsolutePath(), false);
				this.mergeSvnProperties(sourceLocalPath, destLocalPath);

				break;

			default:
				throw new IllegalStateException("mergeAction_Add: NodeKind '" + NodeKind.getNodeKindName(changeItem.getNodeKind()) + "' is not supported.");
			}
		}		

		// Set revision info property
		Info svnInfo = svnSource.info(sourceLocalPath.getAbsolutePath());
		RevisionInfo revInfo = new RevisionInfo();
		revInfo.source = svnInfo.getUrl();
		revInfo.revision = svnInfo.getRevision();
		svnDestination.propertySet(destLocalPath.getAbsolutePath(), this.propNameRevInfo, RevisionInfo.serialize(revInfo), false);
	}

	private void mergeAction_Modify(ChangePath changeItem) throws ClientException, IOException, JAXBException {

		String itemPath = changeItem.getPath().replace(_initInfo.relativePath, "");

		File sourceLocalPath = new File(sourceLocalCopyPath, itemPath);
		File destLocalPath = new File(destinationLocalCopyPath, itemPath);
		//String itemLocalPathStr = itemLocalPath.getAbsolutePath();

		if(changeItem.getCopySrcPath() != null) { // branching

			throw new IllegalStateException("Not impelement");
		}
		else {	// creating

			int kind = changeItem.getNodeKind();
			if(kind == NodeKind.unknown) {	// old version of svn server not set kind, try get it from svnInfo

				Info svnInfo = svnSource.info(sourceLocalPath.getAbsolutePath());
				kind = svnInfo.getNodeKind();
			}

			switch(kind) {
			case NodeKind.dir:
				this.mergeSvnProperties(sourceLocalPath, destLocalPath);
				break;

			case NodeKind.file:

				FileInputStream fis = new FileInputStream(sourceLocalPath);
				FileOutputStream fos = new FileOutputStream(destLocalPath);
				byte[] buf = new byte[1024];
				int i = 0;
				while((i=fis.read(buf))!=-1) { fos.write(buf, 0, i); }
				fis.close();
				fos.close();

				this.mergeSvnProperties(sourceLocalPath, destLocalPath);
				break;

			default:
				throw new IllegalStateException("mergeAction_Modify: NodeKind '" + NodeKind.getNodeKindName(changeItem.getNodeKind()) + "' is not supported.");
			}

			// Set revision info property
			Info svnInfo = svnSource.info(sourceLocalPath.getAbsolutePath());
			RevisionInfo revInfo = new RevisionInfo();
			revInfo.source = svnInfo.getUrl();
			revInfo.revision = svnInfo.getRevision();
			svnDestination.propertySet(destLocalPath.getAbsolutePath(), this.propNameRevInfo, RevisionInfo.serialize(revInfo), false);
		}
	}

	private void mergeAction_Delete(ChangePath changeItem) throws ClientException, IOException {

		String itemPath = changeItem.getPath().replace(_initInfo.relativePath, "");
		if("".equals(itemPath))
			throw new IllegalStateException("mergeAction_Add : no file to delete");

		//File sourceLocalPath = new File(sourceLocalCopyPath, itemPath);
		File destLocalPath = new File(destinationLocalCopyPath, itemPath);

		if(changeItem.getCopySrcPath() != null) { // branching

			throw new IllegalStateException("Not impelement");
		}
		else {	// creating

			svnDestination.remove(new String[] { destLocalPath.getAbsolutePath() }, null, true);
		}
	}
}
